<?php
/**
 * Zend Framework
 *
 * LICENSE
 *
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 *
 * @category   Zend
 * @package    Zend_Json
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 * @version    $Id: Decoder.php 16541 2009-07-07 06:59:03Z bkarwin $
 */

/**
 * @see Zend_Json
 */
require_once 'Zend/Json.php';

/**
 * Decode JSON encoded string to PHP variable constructs
 *
 * @category   Zend
 * @package    Zend_Json
 * @copyright  Copyright (c) 2005-2009 Zend Technologies USA Inc. (http://www.zend.com)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Json_Decoder
{
	/**
	 * Parse tokens used to decode the JSON object. These are not
	 * for public consumption, they are just used internally to the
	 * class.
	 */
	const EOF         = 0;
	const DATUM        = 1;
	const LBRACE    = 2;
	const LBRACKET    = 3;
	const RBRACE     = 4;
	const RBRACKET    = 5;
	const COMMA       = 6;
	const COLON        = 7;

	/**
	 * Use to maintain a "pointer" to the source being decoded
	 *
	 * @var string
	 */
	protected $_source;

	/**
	 * Caches the source length
	 *
	 * @var int
	 */
	protected $_sourceLength;

	/**
	 * The offset within the souce being decoded
	 *
	 * @var int
	 *
	 */
	protected $_offset;

	/**
	 * The current token being considered in the parser cycle
	 *
	 * @var int
	 */
	protected $_token;

	/**
	 * Flag indicating how objects should be decoded
	 *
	 * @var int
	 * @access protected
	 */
	protected $_decodeType;

	/**
	 * Constructor
	 *
	 * @param string $source String source to decode
	 * @param int $decodeType How objects should be decoded -- see
	 * {@link Zend_Json::TYPE_ARRAY} and {@link Zend_Json::TYPE_OBJECT} for
	 * valid values
	 * @return void
	 */
	protected function __construct($source, $decodeType)
	{
		// Set defaults
		$this->_source       = self::decodeUnicodeString($source);
		$this->_sourceLength = strlen($this->_source);
		$this->_token        = self::EOF;
		$this->_offset       = 0;

		// Normalize and set $decodeType
		if (!in_array($decodeType, array(Zend_Json::TYPE_ARRAY, Zend_Json::TYPE_OBJECT)))
		{
			$decodeType = Zend_Json::TYPE_ARRAY;
		}
		$this->_decodeType   = $decodeType;

		// Set pointer at first token
		$this->_getNextToken();
	}

	/**
	 * Decode a JSON source string
	 *
	 * Decodes a JSON encoded string. The value returned will be one of the
	 * following:
	 *        - integer
	 *        - float
	 *        - boolean
	 *        - null
	 *      - StdClass
	 *      - array
	 *         - array of one or more of the above types
	 *
	 * By default, decoded objects will be returned as associative arrays; to
	 * return a StdClass object instead, pass {@link Zend_Json::TYPE_OBJECT} to
	 * the $objectDecodeType parameter.
	 *
	 * Throws a Zend_Json_Exception if the source string is null.
	 *
	 * @static
	 * @access public
	 * @param string $source String to be decoded
	 * @param int $objectDecodeType How objects should be decoded; should be
	 * either or {@link Zend_Json::TYPE_ARRAY} or
	 * {@link Zend_Json::TYPE_OBJECT}; defaults to TYPE_ARRAY
	 * @return mixed
	 * @throws Zend_Json_Exception
	 */
	public static function decode($source = null, $objectDecodeType = Zend_Json::TYPE_ARRAY)
	{
		if (null === $source) {
			require_once 'Zend/Json/Exception.php';
			throw new Zend_Json_Exception('Must specify JSON encoded source for decoding');
		} elseif (!is_string($source)) {
			require_once 'Zend/Json/Exception.php';
			throw new Zend_Json_Exception('Can only decode JSON encoded strings');
		}

		$decoder = new self($source, $objectDecodeType);

		return $decoder->_decodeValue();
	}


	/**
	 * Recursive driving rountine for supported toplevel tops
	 *
	 * @return mixed
	 */
	protected function _decodeValue()
	{
		switch ($this->_token) {
			case self::DATUM:
				$result  = $this->_tokenValue;
				$this->_getNextToken();
				return($result);
				break;
			case self::LBRACE:
				return($this->_decodeObject());
				break;
			case self::LBRACKET:
				return($this->_decodeArray());
				break;
			default:
				return null;
				break;
		}
	}

	/**
	 * Decodes an object of the form:
	 *  { "attribute: value, "attribute2" : value,...}
	 *
	 * If Zend_Json_Encoder was used to encode the original object then
	 * a special attribute called __className which specifies a class
	 * name that should wrap the data contained within the encoded source.
	 *
	 * Decodes to either an array or StdClass object, based on the value of
	 * {@link $_decodeType}. If invalid $_decodeType present, returns as an
	 * array.
	 *
	 * @return array|StdClass
	 */
	protected function _decodeObject()
	{
		$members = array();
		$tok = $this->_getNextToken();

		while ($tok && $tok != self::RBRACE) {
			if ($tok != self::DATUM || ! is_string($this->_tokenValue)) {
				require_once 'Zend/Json/Exception.php';
				throw new Zend_Json_Exception('Missing key in object encoding: ' . $this->_source);
			}

			$key = $this->_tokenValue;
			$tok = $this->_getNextToken();

			if ($tok != self::COLON) {
				require_once 'Zend/Json/Exception.php';
				throw new Zend_Json_Exception('Missing ":" in object encoding: ' . $this->_source);
			}

			$tok = $this->_getNextToken();
			$members[$key] = $this->_decodeValue();
			$tok = $this->_token;

			if ($tok == self::RBRACE) {
				break;
			}

			if ($tok != self::COMMA) {
				require_once 'Zend/Json/Exception.php';
				throw new Zend_Json_Exception('Missing "," in object encoding: ' . $this->_source);
			}

			$tok = $this->_getNextToken();
		}

		switch ($this->_decodeType) {
			case Zend_Json::TYPE_OBJECT:
				// Create new StdClass and populate with $members
				$result = new StdClass();
				foreach ($members as $key => $value) {
					$result->$key = $value;
				}
				break;
			case Zend_Json::TYPE_ARRAY:
			default:
				$result = $members;
				break;
		}

		$this->_getNextToken();
		return $result;
	}

	/**
	 * Decodes a JSON array format:
	 *    [element, element2,...,elementN]
	 *
	 * @return array
	 */
	protected function _decodeArray()
	{
		$result = array();
		$starttok = $tok = $this->_getNextToken(); // Move past the '['
		$index  = 0;

		while ($tok && $tok != self::RBRACKET) {
			$result[$index++] = $this->_decodeValue();

			$tok = $this->_token;

			if ($tok == self::RBRACKET || !$tok) {
				break;
			}

			if ($tok != self::COMMA) {
				require_once 'Zend/Json/Exception.php';
				throw new Zend_Json_Exception('Missing "," in array encoding: ' . $this->_source);
			}

			$tok = $this->_getNextToken();
		}

		$this->_getNextToken();
		return($result);
	}


	/**
	 * Removes whitepsace characters from the source input
	 */
	protected function _eatWhitespace()
	{
		if (preg_match(
                '/([\t\b\f\n\r ])*/s',
		$this->_source,
		$matches,
		PREG_OFFSET_CAPTURE,
		$this->_offset)
		&& $matches[0][1] == $this->_offset)
		{
			$this->_offset += strlen($matches[0][0]);
		}
	}


	/**
	 * Retrieves the next token from the source stream
	 *
	 * @return int Token constant value specified in class definition
	 */
	protected function _getNextToken()
	{
		$this->_token      = self::EOF;
		$this->_tokenValue = null;
		$this->_eatWhitespace();

		if ($this->_offset >= $this->_sourceLength) {
			return(self::EOF);
		}

		$str        = $this->_source;
		$str_length = $this->_sourceLength;
		$i          = $this->_offset;
		$start      = $i;

		switch ($str{$i}) {
			case '{':
				$this->_token = self::LBRACE;
				break;
			case '}':
				$this->_token = self::RBRACE;
				break;
			case '[':
				$this->_token = self::LBRACKET;
				break;
			case ']':
				$this->_token = self::RBRACKET;
				break;
			case ',':
				$this->_token = self::COMMA;
				break;
			case ':':
				$this->_token = self::COLON;
				break;
			case  '"':
				$result = '';
				do {
					$i++;
					if ($i >= $str_length) {
						break;
					}

					$chr = $str{$i};

					if ($chr == '\\') {
						$i++;
						if ($i >= $str_length) {
							break;
						}
						$chr = $str{$i};
						switch ($chr) {
							case '"' :
								$result .= '"';
								break;
							case '\\':
								$result .= '\\';
								break;
							case '/' :
								$result .= '/';
								break;
							case 'b' :
								$result .= chr(8);
								break;
							case 'f' :
								$result .= chr(12);
								break;
							case 'n' :
								$result .= chr(10);
								break;
							case 'r' :
								$result .= chr(13);
								break;
							case 't' :
								$result .= chr(9);
								break;
							case '\'' :
								$result .= '\'';
								break;
							default:
								require_once 'Zend/Json/Exception.php';
								throw new Zend_Json_Exception("Illegal escape "
								.  "sequence '" . $chr . "'");
						}
					} elseif($chr == '"') {
						break;
					} else {
						$result .= $chr;
					}
				} while ($i < $str_length);

				$this->_token = self::DATUM;
				//$this->_tokenValue = substr($str, $start + 1, $i - $start - 1);
				$this->_tokenValue = $result;
				break;
			case 't':
				if (($i+ 3) < $str_length && substr($str, $start, 4) == "true") {
					$this->_token = self::DATUM;
				}
				$this->_tokenValue = true;
				$i += 3;
				break;
			case 'f':
				if (($i+ 4) < $str_length && substr($str, $start, 5) == "false") {
					$this->_token = self::DATUM;
				}
				$this->_tokenValue = false;
				$i += 4;
				break;
			case 'n':
				if (($i+ 3) < $str_length && substr($str, $start, 4) == "null") {
					$this->_token = self::DATUM;
				}
				$this->_tokenValue = NULL;
				$i += 3;
				break;
		}

		if ($this->_token != self::EOF) {
			$this->_offset = $i + 1; // Consume the last token character
			return($this->_token);
		}

		$chr = $str{$i};
		if ($chr == '-' || $chr == '.' || ($chr >= '0' && $chr <= '9')) {
			if (preg_match('/-?([0-9])*(\.[0-9]*)?((e|E)((-|\+)?)[0-9]+)?/s',
			$str, $matches, PREG_OFFSET_CAPTURE, $start) && $matches[0][1] == $start) {

				$datum = $matches[0][0];

				if (is_numeric($datum)) {
					if (preg_match('/^0\d+$/', $datum)) {
						require_once 'Zend/Json/Exception.php';
						throw new Zend_Json_Exception("Octal notation not supported by JSON (value: $datum)");
					} else {
						$val  = intval($datum);
						$fVal = floatval($datum);
						$this->_tokenValue = ($val == $fVal ? $val : $fVal);
					}
				} else {
					require_once 'Zend/Json/Exception.php';
					throw new Zend_Json_Exception("Illegal number format: $datum");
				}

				$this->_token = self::DATUM;
				$this->_offset = $start + strlen($datum);
			}
		} else {
			require_once 'Zend/Json/Exception.php';
			throw new Zend_Json_Exception('Illegal Token');
		}

		return($this->_token);
	}

	/**
	 * Decode Unicode Characters from \u0000 ASCII syntax.
	 *
	 * This algorithm was originally developed for the
	 * Solar Framework by Paul M. Jones
	 *
	 * @link   http://solarphp.com/
	 * @link   http://svn.solarphp.com/core/trunk/Solar/Json.php
	 * @param  string $value
	 * @return string
	 */
	public static function decodeUnicodeString($chrs)
	{
		$delim       = substr($chrs, 0, 1);
		$utf8        = '';
		$strlen_chrs = strlen($chrs);

		for($i = 0; $i < $strlen_chrs; $i++) {

			$substr_chrs_c_2 = substr($chrs, $i, 2);
			$ord_chrs_c = ord($chrs[$i]);

			switch (true) {
				case preg_match('/\\\u[0-9A-F]{4}/i', substr($chrs, $i, 6)):
					// single, escaped unicode character
					$utf16 = chr(hexdec(substr($chrs, ($i + 2), 2)))
					. chr(hexdec(substr($chrs, ($i + 4), 2)));
					$utf8 .= self::_utf162utf8($utf16);
					$i += 5;
					break;
				case ($ord_chrs_c >= 0x20) && ($ord_chrs_c <= 0x7F):
					$utf8 .= $chrs{$i};
					break;
				case ($ord_chrs_c & 0xE0) == 0xC0:
					// characters U-00000080 - U-000007FF, mask 110XXXXX
					//see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
					$utf8 .= substr($chrs, $i, 2);
					++$i;
					break;
				case ($ord_chrs_c & 0xF0) == 0xE0:
					// characters U-00000800 - U-0000FFFF, mask 1110XXXX
					// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
					$utf8 .= substr($chrs, $i, 3);
					$i += 2;
					break;
				case ($ord_chrs_c & 0xF8) == 0xF0:
					// characters U-00010000 - U-001FFFFF, mask 11110XXX
					// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
					$utf8 .= substr($chrs, $i, 4);
					$i += 3;
					break;
				case ($ord_chrs_c & 0xFC) == 0xF8:
					// characters U-00200000 - U-03FFFFFF, mask 111110XX
					// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
					$utf8 .= substr($chrs, $i, 5);
					$i += 4;
					break;
				case ($ord_chrs_c & 0xFE) == 0xFC:
					// characters U-04000000 - U-7FFFFFFF, mask 1111110X
					// see http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
					$utf8 .= substr($chrs, $i, 6);
					$i += 5;
					break;
			}
		}

		return $utf8;
	}

	/**
	 * Convert a string from one UTF-16 char to one UTF-8 char.
	 *
	 * Normally should be handled by mb_convert_encoding, but
	 * provides a slower PHP-only method for installations
	 * that lack the multibye string extension.
	 *
	 * This method is from the Solar Framework by Paul M. Jones
	 *
	 * @link   http://solarphp.com
	 * @param  string $utf16 UTF-16 character
	 * @return string UTF-8 character
	 */
	protected static function _utf162utf8($utf16)
	{
		// Check for mb extension otherwise do by hand.
		if( function_exists('mb_convert_encoding') ) {
			return mb_convert_encoding($utf16, 'UTF-8', 'UTF-16');
		}

		$bytes = (ord($utf16{0}) << 8) | ord($utf16{1});

		switch (true) {
			case ((0x7F & $bytes) == $bytes):
				// this case should never be reached, because we are in ASCII range
				// see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
				return chr(0x7F & $bytes);

			case (0x07FF & $bytes) == $bytes:
				// return a 2-byte UTF-8 character
				// see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
				return chr(0xC0 | (($bytes >> 6) & 0x1F))
				. chr(0x80 | ($bytes & 0x3F));

			case (0xFFFF & $bytes) == $bytes:
				// return a 3-byte UTF-8 character
				// see: http://www.cl.cam.ac.uk/~mgk25/unicode.html#utf-8
				return chr(0xE0 | (($bytes >> 12) & 0x0F))
				. chr(0x80 | (($bytes >> 6) & 0x3F))
				. chr(0x80 | ($bytes & 0x3F));
		}

		// ignoring UTF-32 for now, sorry
		return '';
	}
}

